package org.unidata.mdm.rest.v1.meta.service.entities;

import static org.unidata.mdm.rest.v1.meta.converter.LookupEntityDefToLookupEntityDefinitionConverter.toLookupEntityRO;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.model.LookupElement;
import org.unidata.mdm.core.type.model.RegisterElement;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.context.GetDataModelContext;
import org.unidata.mdm.meta.context.UpsertDataModelContext;
import org.unidata.mdm.meta.dto.GetLookupDTO;
import org.unidata.mdm.meta.dto.GetModelDTO;
import org.unidata.mdm.meta.type.model.entities.LookupEntity;
import org.unidata.mdm.rest.v1.meta.converter.LookupEntityDefToLookupEntityDefinitionConverter;
import org.unidata.mdm.rest.v1.meta.converter.LookupEntityDefinitionToLookupEntityDefConverter;
import org.unidata.mdm.rest.v1.meta.exception.MetaRestExceptionIds;
import org.unidata.mdm.rest.v1.meta.ro.entities.LookupEntityRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.lookup.DeleteLookupResultRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.lookup.GetLookupLinksResultRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.lookup.GetLookupResultRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.lookup.GetLookupsResultRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.lookup.UpsertLookupRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.entities.lookup.UpsertLookupResultRO;
import org.unidata.mdm.rest.v1.meta.ro.references.EntityReferenceRO;
import org.unidata.mdm.rest.v1.meta.ro.references.LookupReferenceRO;
import org.unidata.mdm.rest.v1.meta.ro.references.ReferenceInfoRO;
import org.unidata.mdm.system.exception.PlatformBusinessException;
import org.unidata.mdm.system.service.TouchService;
import org.unidata.mdm.system.type.touch.Touch;
import org.unidata.mdm.system.type.touch.TouchParams;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;

/**
 * Lookup rest controller
 *
 * @author Alexandr Serov
 * @since 23.11.2020
 **/
@Path(LookupEntityRestService.SERVICE_PATH)
@Consumes({"application/json"})
@Produces({"application/json"})
public class LookupEntityRestService extends AbstractMetaEntitiesRestService {

    public static final String SERVICE_PATH = "lookup-entities";

    public static final String SERVICE_TAG = "lookup-entities";

    private static final String LOOKUP_NOT_FOUND_ERROR = "Lookup %s not found (draftId: %s)";

    private static final Touch<Boolean> TOUCH_HAS_LOOKUP_DATA
        = Touch.builder(Boolean.class)
            .touchName("[touch-has-lookup-data]")
            .paramType("lookup-name", String.class)
            .build();
    /**
     * The TS.
     */
    @Autowired
    private TouchService touchService;

    @GET
    @Path("{id}")
    @Operation(description = "Gets a lookup by ID.", method = HttpMethod.GET, tags = SERVICE_TAG)
    public GetLookupResultRO findByName(@Parameter(description = "ID.", in = ParameterIn.PATH)
                                        @PathParam("id") String id,
                                        @Parameter(description = "Draft ID. Optional.", in = ParameterIn.QUERY)
                                        @QueryParam("draftId") @DefaultValue("0") Long draftId,
                                        @Parameter(description = "Check for data existance.", in = ParameterIn.QUERY)
                                        @QueryParam("checkData") @DefaultValue("true") Boolean checkData) {
        GetLookupResultRO result = new GetLookupResultRO();
        if (BooleanUtils.isTrue(checkData)) {
            result.setLookupEntity(dataIndicator(toLookupEntityRO(lookupByName(id, draftId).getLookup())));
        } else {
            GetLookupDTO dto = findLookupByName(id, draftId);
            LookupEntity entity;
            if (dto != null && (entity = dto.getLookup()) != null) {
                result.setLookupEntity(toLookupEntityRO(entity));
            }
        }
        return result;
    }

    /**
     * Gets a list of lookup entities.
     *
     * @param draftId draftId
     * @return list of entity info
     */
    @GET
    @Operation(
        description = "Lookup list.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetLookupsResultRO findAll(
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId,
            @Parameter(description = "Check for data existance.", in = ParameterIn.QUERY) @QueryParam("checkData") @DefaultValue("true") Boolean checkData) {

        GetLookupsResultRO result = new GetLookupsResultRO();
        List<GetLookupDTO> lookups = findEntitiesByDraft(draftId, GetModelDTO::getLookups);
        if (!lookups.isEmpty()) {
            result.setLookupEntities(lookups.stream()
                .map(GetLookupDTO::getLookup)
                .map(LookupEntityDefToLookupEntityDefinitionConverter::toLookupEntityRO)
                .map(ro -> checkData.booleanValue() ? dataIndicator(ro) : ro)
                .filter(el -> allow(el.getName()))
                .collect(Collectors.toList()));
        } else {
            result.setLookupEntities(Collections.emptyList());
        }
        return result;
    }


    /**
     * Get all records with lookup link to sending lookup id
     *
     * @param lookupId lookup id
     * @return records linked to lookup
     */
    @GET
    @Path("lookup-links/{id}")
    @Operation(
        description = "Gets objects, having references to this lookup.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG
    )
    public GetLookupLinksResultRO findLookupLinks(@Parameter(description = "Lookup id.", in = ParameterIn.PATH) @PathParam("id") String lookupId) {
        EntityElement entityElement = instance(Descriptors.DATA).getLookup(lookupId);
        LookupElement lookupElement;
        GetLookupLinksResultRO result = new GetLookupLinksResultRO();
        if (entityElement != null && (lookupElement = entityElement.getLookup()) != null) {
            Map<RegisterElement, Set<AttributeElement>> dependencies = ObjectUtils.defaultIfNull(lookupElement.getReferencingRegisters(), Collections.emptyMap());
            Map<LookupElement, Set<AttributeElement>> lookupDependencies = ObjectUtils.defaultIfNull(lookupElement.getReferencingLookups(), Collections.emptyMap());
            List<ReferenceInfoRO> refs = new ArrayList<>();
            dependencies.keySet().forEach(e -> {
                ReferenceInfoRO referenceInfo = new ReferenceInfoRO();
                referenceInfo.setTargetKey(new LookupReferenceRO(lookupId));
                referenceInfo.setSourceKey(new EntityReferenceRO(e.getName()));
                referenceInfo.setSourceType(referenceInfo.getSourceKey().keyType().getName());
                referenceInfo.setTargetType(referenceInfo.getTargetKey().keyType().getName());
                refs.add(referenceInfo);
            });
            lookupDependencies.keySet().forEach(e -> {
                ReferenceInfoRO referenceInfo = new ReferenceInfoRO();
                referenceInfo.setTargetKey(new LookupReferenceRO(lookupId));
                referenceInfo.setSourceKey(new LookupReferenceRO(e.getName()));
                referenceInfo.setSourceType(referenceInfo.getSourceKey().keyType().getName());
                referenceInfo.setTargetType(referenceInfo.getTargetKey().keyType().getName());
                refs.add(referenceInfo);
            });
            result.setLinks(refs);
        }
        return result;
    }

    @POST
    @Path("upsert")
    @Operation(
        description = "Create or update a lookup.",
        method = HttpMethod.PUT,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = UpsertLookupRequestRO.class)), description = "Upsert request."),
        tags = SERVICE_TAG
    )
    public UpsertLookupResultRO upsert(UpsertLookupRequestRO req) {
        Objects.requireNonNull(req, "Request can't be null");
        UpsertLookupResultRO result = new UpsertLookupResultRO();
        LookupEntityRO src = notNull("lookupEntity", req.getLookupEntity());
        Long draftId = resolveDraftId(req.getDraftId());
        metaModelService.upsert(UpsertDataModelContext.builder()
            .lookupEntitiesUpdate(LookupEntityDefinitionToLookupEntityDefConverter.convertToLookupEntity(src))
            .draftId(draftId)
            .build());
        result.setDraftId(draftId);
        return result;
    }

    private <T> List<T> findEntitiesByDraft(Long draftId, Function<GetModelDTO, List<T>> mapper) {
        return findEntitiesByRequest(GetDataModelContext.builder()
                .draftId(resolveDraftId(draftId))
                .allLookups(true)
                .build(), mapper);
    }

    /**
     * Delete lookup entity.
     *
     * @param id id to delete
     * @return 200 Ok
     */
    @DELETE
    @Path("{id}")
    @Operation(
        description = "Removes a lookup.",
        method = HttpMethod.DELETE,
        parameters = {
            @Parameter(description = "Lookup id.", in = ParameterIn.PATH, name = "id"),
            @Parameter(description = "Existing draft id. Optional.", in = ParameterIn.QUERY, name = "draftId")
        }, tags = SERVICE_TAG
    )
    public DeleteLookupResultRO delete(@PathParam("id") String id, @QueryParam("draftId") @DefaultValue("0") Long draftId) {
        DeleteLookupResultRO result = new DeleteLookupResultRO();
        GetLookupDTO dto = lookupByName(id, draftId);
        metaModelService.upsert(UpsertDataModelContext.builder()
            .lookupEntitiesDelete(id)
            .draftId(resolveDraftId(draftId))
            .build());
        result.setId(id);
        return result;
    }

    private GetLookupDTO lookupByName(String entityName, Long draftId) {
        GetLookupDTO result = findLookupByName(entityName, draftId);
        if (result == null) {
            throw new PlatformBusinessException(String.format(LOOKUP_NOT_FOUND_ERROR, entityName, draftId), MetaRestExceptionIds.EX_META_DATA_LOOKUP_NOT_FOUND);
        }
        return result;
    }

    private GetLookupDTO findLookupByName(String entityName, Long draftId) {
        return findFirstEntityByRequest(GetDataModelContext.builder()
            .lookupIds(Collections.singletonList(entityName))
            .draftId(resolveDraftId(draftId))
            .build(), GetModelDTO::getLookups);
    }

    private LookupEntityRO dataIndicator(LookupEntityRO ro) {

        if (Objects.nonNull(ro)) {

            ro.setHasData(BooleanUtils.toBoolean(
                    touchService.singleTouch(new TouchParams<>(TOUCH_HAS_LOOKUP_DATA)
                            .with("lookup-name", ro.getName()))));
        }

        return ro;
    }
}
